diff --git a/src/main/scala/scim/Library.scala b/src/main/scala/scim/Library.scala index 5f5c202..94e15d8 100644 --- a/src/main/scala/scim/Library.scala +++ b/src/main/scala/scim/Library.scala @@ -1,17 +1,120 @@ package scim package lib +// java imports import java.io.{File, FileInputStream} + +// scala imports import scala.collection.convert.wrapAll._ +import scala.collection.mutable +import scala.collection.JavaConverters._ + // external imports import org.yaml.snakeyaml.Yaml +package object printUtil { + def indentCollection(s: String) : String = { + var indent = 0 + var skip = false + var bracket = false + var output = "" + + def closureIndentCollection(c: Char) : Unit = { + if(c == '(') { + indent += 1 + output += "(\n" + "\t" * indent + } else if(c == ')') { + indent -= 1 + if(bracket) output += "\n" + "\t" * indent + output += ")" + bracket = true + } else if(skip) { + skip = false + } else if(c == ',') { + skip = true + bracket = false + output += "," + "\n" + "\t" * indent + } else { + output += c.toString + } + } + + s.foreach(c => closureIndentCollection(c)) + output + } +} + +package object convertUtil { + def javaArrayListToSeq(javaArray: Any) : Option[Seq[Any]] = { + javaArray match { + /* While LinearSeq(List) for array.length == 2 is faster than than IndexedSeq(Vector) when accessed via tail/head + * this would convert a lot of elements to List that are accessed via foreach and not tail/head lookup + * as we don't know what they're going to be used for at this point, it makes more sense to just use vectors + * https://docs.scala-lang.org/tutorials/FAQ/collections.html + * https://docs.scala-lang.org/overviews/collections/performance-characteristics.html + */ + case array: java.util.ArrayList[_] => Some(array.asScala.toIndexedSeq) + case _ => None + } + } +} + +package object mapUtil { + + def javaHashMapToScalaMap[K, V](javaMap: java.util.LinkedHashMap[_, _]) : Map[K, V] = { + mapAsScalaMap(javaMap).toMap.asInstanceOf[Map[K, V]] + } + + def anyToNestedMap[K](map: Any, depthLimit: Int = 100) : Map[K, Any] = { + + def closureAnyToNestedMap(nestedValue: Any, depth: Int = 0) : Any = { + if(depth > depthLimit) return nestedValue // crude way to avoid a circular map + anyToMap[K, Any](nestedValue) match { + case Some(nestedMap) => nestedMap.map(kv => kv._1 -> closureAnyToNestedMap(kv._2, depth + 1)) + case _ => + convertUtil.javaArrayListToSeq(nestedValue) match { + case Some(nestedSeq) => nestedSeq.map(v => closureAnyToNestedMap(v, depth + 1)) + case _ => nestedValue + } + } + } + + anyToMap[K, Any](map) match { + case Some(nestedMap: Map[K, Any]) => nestedMap.map(kv => (kv._1 -> closureAnyToNestedMap(kv._2))) + case _ => Map[K, Any]() // if no map was given + } + } + + def anyToMutableMap[K, V](mapAny: Any) : Option[mutable.Map[K, V]] = { + // TODO: use TypeTag to work around type erasure to check for T[K, V] + // http://squidarth.com/scala/types/2019/01/11/type-erasure-scala.html + mapAny match { + case map: Map[_, _] => Some(mutable.Map() ++ map.asInstanceOf[Map[K, V]]) + case map: mutable.Map[_, _] => Some(map.asInstanceOf[mutable.Map[K, V]]) + case map: java.util.LinkedHashMap[_, _] => Some(mapAsScalaMap(map).asInstanceOf[mutable.Map[K, V]]) + case _ => None + } + } + + def anyToMap[K, V](mapAny: Any) : Option[Map[K, V]] = { + // TODO: use TypeTag to work around type erasure to check for T[K, V] + // http://squidarth.com/scala/types/2019/01/11/type-erasure-scala.html + mapAny match { + case map: Map[_, _] => Some(map.asInstanceOf[Map[K, V]]) + case map: mutable.Map[_, _] => Some(map.toMap.asInstanceOf[Map[K, V]]) + case map: java.util.LinkedHashMap[_, _] => Some(javaHashMapToScalaMap[K, V](map)) + case _ => None + } + } + +} + package object yml { val yaml = new Yaml // TODO: handle errors (invalid file/format) - def parse(file:String) : Map[Nothing, Nothing] = { + def parse(file:String) : Map[String, Any] = { val fis = new FileInputStream(new File(file)) - val list = mapAsScalaMap(yaml.load(fis)) - return list.toMap + mapUtil.anyToNestedMap[String](yaml.load(fis)) } } + diff --git a/src/main/scala/scim/Main.scala b/src/main/scala/scim/Main.scala index 4d0d291..cc0462f 100644 --- a/src/main/scala/scim/Main.scala +++ b/src/main/scala/scim/Main.scala @@ -5,7 +5,8 @@ import scim.lib._ object Main { def main(args: Array[String]): Unit = { val config = "docs/wiki/config/config.yml" - val list = yml.parse(config) - println(list) + val list: Map[String, Any] = yml.parse(config) + + println(printUtil.indentCollection(list.toString()).toString) } } \ No newline at end of file diff --git a/src/test/scala/scim/TestSuite.scala b/src/test/scala/scim/TestSuite.scala new file mode 100644 index 0000000..09e79de --- /dev/null +++ b/src/test/scala/scim/TestSuite.scala @@ -0,0 +1,28 @@ +package scim + +import org.scalatest.FunSuite +import org.junit.runner.RunWith +import org.scalatestplus.junit.JUnitRunner + +@RunWith(classOf[JUnitRunner]) +class TestSuite extends FunSuite { + + var testObject = "scim.lib.yml.parse" + test(testObject + ": parsing test") { + // test configuration + val config = "src/test/scala/scim/files/test.yml" + val mapCheck: Map[String, Any] = + Map( + "simulation" -> Map("duration" -> 500, "tickfactor" -> 10, "loop" -> true, "loopcount" -> 20), + "sensors" -> Map("blocks" -> List(Map("start" -> Vector(8.2, 1, 5), "end" -> List(3, 3), "count" -> 30))) + ) + // sanity check, to prevent a false positive if both are empty + assert(!mapCheck.isEmpty, testObject + ": check list is empty") + + // test + val mapTest: Map[String, Any] = scim.lib.yml.parse(config) + // Note: will assert to true even if numerical type doesnt match (Int, Float, Double; eg: 5.0 == 5 -> true) + assert(mapTest == mapCheck, testObject + ": test map is not equal to check map") + } + +} diff --git a/src/test/scala/scim/files/test.yml b/src/test/scala/scim/files/test.yml new file mode 100644 index 0000000..4e6947c --- /dev/null +++ b/src/test/scala/scim/files/test.yml @@ -0,0 +1,10 @@ +simulation: + duration: 500 + tickfactor: 10 + loop: true + loopcount: 20 +sensors: + blocks: + - start: [8.2, 1, 5] + end: [3, 3] + count: 30 \ No newline at end of file