
.13.e3.source-code.kson.scala Maven / Gradle / Ivy
/*
Copyright 2010 Aaron J. Radke
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 cc.drx
import scala.collection.concurrent.{TrieMap => CCTrieMap}
/** Prop: This top level class offers a quick lookup mechanism based off of Kson.watched files and scoped string map lookups as follows:
* ex: Prop.of(File.home/".kson")/"email" | "default email"
* WARNING: mutable singleton properties lie within here.
* */
object Prop{
//hide a bit a mutability here with caching
import Implicit.ec
private val watchedFileCache:CCTrieMap[File,StringMap] = CCTrieMap.empty
def of(f:File):StringMap = watchedFileCache.getOrElseUpdate(f, Kson.Watched(f))
/**make sure the cache can be cleared*/
def clear() = {watchedFileCache.clear(); println("cleared Prop.watchedFileCache.")}
}
//TODO keep comments and group them together to be added to the next node if they are by themselves or attached to the current node if on the ending line
//TODO add file include/import at a shifted depth and mechanism for watched kson chains
/**Keyed simple object notation is a much simpler and more readable key value notation without needing quotes or commas, and intended for a single line
*
*Kson (Key Separated Object Notation) is a simple format used for many of the configuration formats.
*The 'key' idea is to remain simple like CSV (Comma Separated Values). However, instead of using commas, use ":" as
*the separation delimiter. Then instead of receiving a list of strings for a line of CSV you receive back
*key value pairs for a line of Kson. This provides for a flexible configuration and clean data encoding framework.
*
*Where CSV returns an table of values, Kson can return a list of key value pairs per line or can squash all the key values
*together for a single key value lookup.
*
* For example "person first:Aaron last:Radke language:scala desc:Use simple keys //with some comments"
* gets parsed to Map("/"" -> "person", "first" -> "Aaron", "last" -> "Radke", "language" -> "scala", "quote" -> "Use simple keys")
*
*/
object Kson{
//--constants
val interpolationPat = """\$[\w\.]+""".r
// val pathSeperator = "." //TODO make the path and key separator configurable or think if this is worth it... optionally using '=' may be worth it, or making it configurable at the root of the kson file.
// val keySeperator = ":"
//--constructors
def empty = new Kson(Vector.empty[KsonLine])
def apply(file:File):Kson = apply(file.in)
def apply(string:String):Kson = apply(Input(string))
def apply(in:Input):Kson = {
val lines = for(line <- in.lines; kson = KsonLine(line); if kson.nonEmpty) yield kson
new Kson(lines.toVector)
}
def apply(files:Seq[File]):Kson = files.foldLeft( Kson.empty){case (kson, f) => kson ++ Kson(f) }
/**
* ### simple init
* val kson = Kson.watch(f)
*
* ### init with call back (not needed often since the config is backed by an hash trie cache to automatically hold the newest version)
* val kson = Kson.watch(f).onUpdate{k => println(kson("config.parameter") + "was upated")}
*
* ### delayed init (useful for a global config reference that has not been filled in with a separate file setup arguments)
* val kson = Kson.Watched.empty
* kson.update(File("config.kson")) //updates the file and starts a watcher
*/
def watch(file:File)(implicit ec: ExecutionContext):Watched = Watched(file)
//--wrapper classes (keeps package hierarchy clean
/**mutable that executes call backs on change modifications*/
class Mutable[A <: Mutable[A]] extends StringMap{ //F-bounded polymophism to get type back as self type
self:A => //allow the `this` pointer to represent the inherited subtype for onUpdate fluid api
//--messy mutable fields
private var _kson = Kson.empty //private so only this guy updates it, and initially empty so will work in an monadic environment (foreach over the list will not crash)
private var onUpdateList:List[Kson => Unit] = Nil
//-----
def kson = _kson
def update(k:Kson):Unit = {
_kson = k //use a new kson tree
for(f <- onUpdateList) f(k) //run all the update callbacks
}
//---string map interfaces that provide all the nice features for a config
def getString(key:String):Option[String] = _kson getString key
override def get[T](key:String)(implicit parser:Parsable[T]):Option[T] = _kson get key
override def split[T](key:String,sep:String)(implicit parser:Parsable[T]):Vector[T] = _kson.split(key,sep)
def keys:List[String] = kson.keys //kson.keys:a sorted list of the merged keys based on file order merged.keys:the set of keys (toList is some hash order)
//IDEA add listener that executes call backs for changes in specific kson fields
/**add a function to execute on updates*/
def onUpdate(f: Kson => Unit):A = {onUpdateList ::= f; this} //return this for some fluent api
/**clear the list of callbacks*/
def clear = onUpdateList = Nil
}
/**mutable that executes call backs on change modifications (call backs are added with `onUpdate`)*/
object Watched{
def apply(file:File)(implicit ec: ExecutionContext) = {val watched = new Watched; watched.update(file); watched}
def apply() = new Watched
def empty = new Watched
}
class Watched extends Mutable[Watched]{ //delayed execution the watch
def update(file:File)(implicit ec:ExecutionContext):Unit = {
update(Kson(file)) //TODO should the initial load of of a file call all the call-backs ???
file.watch{f => update(Kson(f))}; //watch the file for modifications and update the Kson with a new parse in the cached mutable version with callbacks
{}
}
}
}
class Kson(val unscopedLines:Vector[KsonLine]) extends StringMap.Cached{
def ++(that:Kson):Kson = new Kson(this.unscopedLines ++ that.unscopedLines)
override def toString = lines.mkString("\n")
def toByteArray:Array[Byte] = Output.toByteArray{out => unscopedLines.foreach(out println _.originString) }
def >>(shift:Int) = new Kson(unscopedLines map (_ >> shift))
def <<(shift:Int) = new Kson(unscopedLines map (_ << shift))
private val emptyScopeAndLines:(List[KsonLine], List[KsonLine]) = ( List(), List() )
lazy val lines:List[KsonLine] = unscopedLines.foldLeft( emptyScopeAndLines ){case ((scope,newLines), line) =>
val thisScope:List[KsonLine] = scope.dropWhile(_.depth >= line.depth)
val newLine = new KsonLine(thisScope, line.roots, line.kvs, line.depth)
val newScope = newLine :: thisScope
(newScope, newLine :: newLines)
}._2.reverse
lazy val interpolated:Kson = new Kson(unscopedLines map {_ interpolate this})
def interpolate(s:String):String = interpolate(0)(s)
/**Generic interpolator function that turns things like $KEY into values if the $KEY does not exist it is left as is
* this allows all $ to remain unless they are followed by a special key..
* TODO make this not depend on Regex
* TODO add bracketed function interpolators
* TODO add implicit op bracketed function interpolators
*/
private def interpolate(recursionDepth:Int)(str:String):String = /*Log(depth,s"interpolate str:$str")*/{
//--depth stop (number of times to check string replace resolution
if(recursionDepth > 100) str else
//--regex
str.replaceAll(Kson.interpolationPat){ pat => getString(pat drop 1) getOrElse pat }
}
private def stripRefTag(ref:String) = if(ref startsWith "$") ref.drop(1) else ref
//get top level merge keyMap interpolated
private def getMerged(key:String, recursionDepth:Int):Option[String] =
//merged.get(key) map interpolate(recursionDepth)
mergedGetWithInheritance(key) map interpolate(recursionDepth)
private def getFromPath(key:String,recursionDepth:Int):Option[String] =
getMerged(key,recursionDepth) orElse
getFromPath(key.split('.').toList,recursionDepth)
private def mergedGetWithInheritance(key:String):Option[String] = {
//-search for inheritance matching key
merged.get(key)
//-search walking back through parents walking up tree
.orElse{
val path = key.split('.').reverse.toList
path match {
case Nil => None
case k :: Nil => merged get k
//-skip parent and go to grand-parent
case k :: x :: xs =>
val inheritedKey = (k :: xs).reverse.mkString(".") //parent-less
mergedGetWithInheritance(inheritedKey)
}
}
}
private def getFromPath(path:List[String],recursionDepth:Int):Option[String] = {
//Log(depth,s"getFromPath path:$path depth:$depth")
val recursionDepthNext = recursionDepth+1
//---recursionDepth death loop
if(recursionDepth > 100) None
//---walk the tree
else {
path match {
case Nil => None
case k :: Nil => getMerged(k,recursionDepthNext) //root element
case k :: x :: xs =>
def dereference(newParent:Option[String]):Option[String] = {
val altScope = newParent map stripRefTag getOrElse k
val altKey = altScope + "." + x
val altPath = altKey :: xs
//-search for specific field
getFromPath(altPath, recursionDepthNext)
}
//-search for exact matching key
def exact = merged get k
//-search for a referenced parent
def interpolated = getMerged(k,recursionDepth) //interpolated parent found
dereference(exact) orElse dereference(interpolated)
}
}
}
lazy val forest:Forest[MTree[KsonLine]] = MTree.nest(lines)(_.depth < _.depth)
private val keyLineNumber:CCTrieMap[String,Double] = CCTrieMap.empty
lazy val merged:Map[String,String] = lines.zipWithIndex.foldLeft(Map.empty[String,String]){case (m, (line,lineNumber)) =>
val prefixes = (line :: line.scope) flatMap {_.roots.headOption} map (_+".")
val prefix:String = prefixes.reverse.mkString
val prefixExt:String = prefixes.drop(1).reverse.mkString
val prefixedLine = for( (k,v) <- line) yield (prefix+k , v) //prefix the head root value to the keys if one exists
val extensions = line.roots zip line.roots.drop(1) map {case (k,v) => (prefixExt+k,"$"+v)}
//println(s"$line | extensions:$extensions roots:${line.roots}") //DEBUG
for( ((k,_),i) <- prefixedLine.zipWithIndex) keyLineNumber(k) = (lineNumber + i/999d) //use doubles to sort keys within lines
val newMerge = m ++ prefixedLine ++ extensions
newMerge
}
//--StringMap.Cached interfaces
lazy val keys:List[String] = merged.keys.toList.sortBy{k => keyLineNumber.getOrElse(k, 0d)}
def getStringNoCache(key:String) = getFromPath(key,0) //provides getString for StringMap
}
object KsonLine{
private def escape(string:String):String =
string
.replaceAllLiterally("::", "$colonColon")
.replaceAllLiterally("://", "$colonSlashSlash")
.replaceAllLiterally("\\:", "$colon")
.replaceAllLiterally("\\=", "$equals")
.replaceAllLiterally("\\/", "$slash")
private def unescape(string:String):String =
string
.replaceAllLiterally("$colonColon", "::")
.replaceAllLiterally("$colonSlashSlash", "://")
.replaceAllLiterally("$colon", ":")
.replaceAllLiterally("$equals", "=")
.replaceAllLiterally("$slash", "/")
private val whitespace = Set(' ', '\t')
private def rootList(root:String):List[String] = root.split("""\s*<-\s*""").toList
@tailrec private def parseRight(baseReversed:String,kvs:List[(String,String)]=Nil):(Option[String],List[(String,String)]) = {
val (v,rest) = baseReversed.trim.span(_ != ':')
if(rest.isEmpty){
val r = unescape(v.trim.reverse)
if(r == "") (None, kvs)
else (Some(r), kvs)
}
else {
val (k,restBase) = rest.tail.trim.span(!_.isWhitespace)
val moreKvs = (unescape(k.trim.reverse),unescape(v.trim.reverse)) :: kvs
if(restBase.trim == "") (None, moreKvs)
else parseRight(restBase, moreKvs)
}
}
def apply(string:String):KsonLine = {
val escapedString = escape(string)
val commentIndex = escapedString indexOf "//"
val base = if(commentIndex < 0) escapedString else escapedString take commentIndex
//distribued depth calculation
// [ ] depth -20 top level??
// [ ] depth -10
val (baseString, depth) = {
//-- calculate indentation based depth
val (spaces, post) = base span whitespace.apply
val spaceDepth = spaces.size
//-- git/properties like header
if((post startsWith "[") && (post endsWith "]"))
post.drop(1).dropRight(1).trim -> (-30 + spaceDepth).sat(-30,0)
//-- colon header:
else if( post.endsWith(":") ){
post.dropRight(1).trim -> (-10 + spaceDepth).sat(-10,0)
}
//-- markdown like headers
else if(post startsWith "#") {
val (bangs, postBang) = post span (_ == '#')
postBang.trim -> (-20 + bangs.size + spaceDepth).sat(-20,0)
}
//-- normal header (positive depth)
else
post.trim -> spaceDepth
}
//---new custom
val (r,kvs) = parseRight(baseString.reverse)
r match {
case Some(r) => new KsonLine(List(), rootList(r), kvs, depth)
case None => new KsonLine(List(), List(), kvs, depth)
}
}
def unapply(line:KsonLine):Option[(List[String],Option[String],List[(String,String)])] = Some((line.scope.flatMap{_.root}, line.root, line.kvs))
def apply(kvs:List[(String,String)]) = new KsonLine(List(), List(), kvs,0)
def apply(root:String,kvs:List[(String,String)]) = new KsonLine(List(), List(root), kvs,0)
}
class KsonLine(val scope:List[KsonLine], val roots:List[String],val kvs:List[(String,String)], val depth:Int) extends Iterable[(String,String)] with StringMap{
lazy val root:Option[String] = if(roots.isEmpty) None else Some(roots mkString " <- ")
//--support StringMap interface
def iterator = kvs.iterator
override lazy val toList = (for(k <- keys; v <- getString(k)) yield (k,v)).toList //required since StringMap and Iterable collide on this method
private lazy val stringMapLineOnly = kvs.toMap
private def keysLineOnly = kvs.map{_._1} //keep order of the keys
private def getStringLineOnly(key:String):Option[String] = stringMapLineOnly get key //TODO for separation of concerns, should this really climb up the parent list for a match
// def keys = kvs.map{_._1} //keep order of the keys
// def getString(key:String):Option[String] = getStringLineOnly(key)
lazy val keys = scope.reverse.flatMap{_.keysLineOnly} ++ keysLineOnly
def getString(key:String) = getString(key, this :: scope) //inherited lookup getStringInherited
/**search the scope upward looking for the key (or the closest parent with the key defined*/
private def getString(key:String, scope:List[KsonLine]):Option[String] = scope match {
case Nil => None //getString(key) //kvs.toMap get key
case p :: ps => p.getStringLineOnly(key) orElse getString(key,ps)
}
def pathList:List[String] = (this :: scope).flatMap{_.root}
// def path:String = if(scope.isEmpty) "" else (this :: scope).flatMap{_.root}
def path:String = pathList.reverse.mkString("/")
override def toString = {
val s = if(scope.isEmpty) "" else scope.flatMap{_.root}.reverse mkString("/")
val k = kvs.foldLeft(root.getOrElse("")){case (r,(k,v)) => s"$r $k:$v"}.trim
s"KsonLine(depth:$depth scope:$s line:{$k})"
}
def originString:String = root ++: kvs.map{case (k,v) => s"$k:$v"} mkString " "
def kvString:String = kvs.map{case (k,v) => s"$k:$v"} mkString " "
override def isEmpty = kvs.isEmpty && root.isEmpty
// override def nonEmpty = !kvs.isEmpty || !root.isEmpty
override def equals(that:Any) = that match {
case that:KsonLine => this.roots == that.roots && this.kvs == that.kvs && this.depth == that.depth
case _ => false
}
override def hashCode:Int = roots.## + 7*depth + 31*kvs.##
def >>(shift:Int) = new KsonLine(scope, roots, kvs, depth + shift)
def <<(shift:Int) = new KsonLine(scope, roots, kvs, depth - shift)
def interpolate(s:String):String = s.replaceAll(Kson.interpolationPat){v => getString(v drop 1, scope) getOrElse v}
def interpolate(s:String,kson:Kson):String = kson.interpolate(interpolate(s))
def interpolate(kson:Kson):KsonLine = {
def f(s:String) = interpolate(s,kson)
new KsonLine(Nil, roots.take(1) map f, kvs map {case (k,v) => (f(k), f(v)) }, depth)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy