[add] added full yml parsing to scala collection objects
- added a simple parsing test - added conversion utils for Any to Map and nested objects - added a quick indented printout util for scala collection objects
This commit is contained in:
@@ -1,17 +1,120 @@
|
|||||||
package scim
|
package scim
|
||||||
package lib
|
package lib
|
||||||
|
|
||||||
|
// java imports
|
||||||
import java.io.{File, FileInputStream}
|
import java.io.{File, FileInputStream}
|
||||||
|
|
||||||
|
// scala imports
|
||||||
import scala.collection.convert.wrapAll._
|
import scala.collection.convert.wrapAll._
|
||||||
|
import scala.collection.mutable
|
||||||
|
import scala.collection.JavaConverters._
|
||||||
|
|
||||||
// external imports
|
// external imports
|
||||||
import org.yaml.snakeyaml.Yaml
|
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 {
|
package object yml {
|
||||||
val yaml = new Yaml
|
val yaml = new Yaml
|
||||||
// TODO: handle errors (invalid file/format)
|
// 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 fis = new FileInputStream(new File(file))
|
||||||
val list = mapAsScalaMap(yaml.load(fis))
|
mapUtil.anyToNestedMap[String](yaml.load(fis))
|
||||||
return list.toMap
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import scim.lib._
|
|||||||
object Main {
|
object Main {
|
||||||
def main(args: Array[String]): Unit = {
|
def main(args: Array[String]): Unit = {
|
||||||
val config = "docs/wiki/config/config.yml"
|
val config = "docs/wiki/config/config.yml"
|
||||||
val list = yml.parse(config)
|
val list: Map[String, Any] = yml.parse(config)
|
||||||
println(list)
|
|
||||||
|
println(printUtil.indentCollection(list.toString()).toString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
28
src/test/scala/scim/TestSuite.scala
Normal file
28
src/test/scala/scim/TestSuite.scala
Normal file
@@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
src/test/scala/scim/files/test.yml
Normal file
10
src/test/scala/scim/files/test.yml
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user