Making objects queryable in Scala

Defining a Queryable trait

In a running program its often nice to have a way of querying and updating the various components of the application. In this example I’ll demonstrate one way you could do this in Scala by defining a Queryable trait. By inheriting this trait, an object can make available properties for reads and updates, and enable traversal to other objects. The data-format used for read results and updates is xml.

The query uses syntax similar to xpath. A few example usages could be:

  • Read a property by config.cache.dir
  • Filter on collection of objects by app.tasks[status='idle'].id
  • Write to a single-valued property by config.screen.width = <width>800</width>
  • Write to a structural-valued property by config.audio.format = <format chan="2" rate="48.0kHz" res="24bit"/>
  • Apply a method by app.playlist.queue = <file type="mp3" loc="http://tunes.com/tune.mp3"/>

Queryable objects can choose between the following kinds of properties:

  • QueryVal for read-only access
  • QueryVar for read-write access
  • QueryDef for an applicable property (similar to write)
  • QueryPath for traversal to named object(s)

The complete sourcecode for the trait is defined as follows.

import scala.util.matching.Regex
import scala.util.parsing.combinator._

/**
 * Query Syntax
 *   query         ::= {query-path `.`} query-leaf query-value?
 *   query-path    ::= name-path filters?
 *   query-leaf    ::= name-leaf
 *   query-value   ::= `=` <a xml="" element="">
 *   name-path     ::= ([a-zA-Z][-a-zA-Z0-9]*)
 *   name-leaf     ::= ([a-zA-Z][-a-zA-Z0-9]*)|[?]
 *   filters       ::= `[` {filter `,`} `]`
 *   filter        ::= filter-key `=` `'` filter-value `'`
 *   filter-key    ::= [a-zA-Z][-a-zA-Z0-9]*
 *   filter-value  ::= `'`  `'`
 */
trait Queryable {
  import Queryable._

  private val queryKeys: () => Seq[xml.Elem] =
    () => {
      for (item <- queryMap.keys.toSeq) yield {
        
      }
    }

  protected var queryMap = Map[String,(String,QueryNode)](
    "?" -> (
      "Lists all queryable keys." -> QueryVal(
        read = queryKeys
      )
    )
  )

  final def query(expr: String): Either[String,xml.Elem] = {
    try {
      val parser = new QueryParser
      parser.parseAll(parser.queryPath, expr) match {
        case parser.Success((path,value), remaining) =>
          Right({ query(path,value) })
        case parser.Error(msg, remaining) =>
          Left("error while parsing query: " + msg)
        case parser.Failure(msg, remaining) =>
          Left("failure while parsing query: " + msg)
        case err =>
          Left("unknown error while parsing query: " + err)
      }
    }
    catch {
      case exp: Exception => Left("exception caught during query: " + exp)
    }
  }

  private def query(path: Seq[Prop], value: Option[xml.Elem]): Seq[xml.Elem] = {
    path.toList match {
      case (key,filters) :: Nil if queryMap contains key =>
        (queryMap(key)._2,value) match {
          case (QueryVal(read),None)           => read()
          case (QueryVar(read,_),None)         => read()
          case (QueryVar(_,write),Some(value)) => write(value)
          case (QueryDef(apply),Some(value))   => apply(value)
          case _                               => Nil
        }
      case (key,filters) :: tail if queryMap contains key =>
        queryMap(key)._2 match {
          case QueryPath(next) =>
            next()
              .filter(node => filters.forall(item => contains(node,item)))
              .flatMap(node => node.query(tail,value))
          case _ => Nil
        }
      case _ => Nil
    }
  }

  private def contains(node: Queryable, check: (String,String)): Boolean = {
    if (node.queryMap.contains(check._1) == false) {
      false
    }
    else {
      val regex = new Regex(check._2)
      node.queryMap(check._1)._2 match {
        case QueryVal(read) =>
          read().headOption.exists(
            item => regex.findPrefixOf(item.text).isDefined
          )
        case QueryVar(read,_) =>
          read().headOption.exists(
            item => regex.findPrefixOf(item.text).isDefined
          )
        case _ => false
      }
    }
  }
}


object Queryable {
  import xml.XML.{loadString => toXml}

  private type Prop = (String,Map[String,String])

  sealed trait QueryNode {
    def typeId(): String
  }
  case class QueryVal(read: () => Seq[xml.Elem]) extends QueryNode {
    def typeId(): String = "val"
  }
  case class QueryVar(read: () => Seq[xml.Elem],
                      write: xml.Elem => Seq[xml.Elem]) extends QueryNode {
    def typeId(): String = "var"
  }
  case class QueryDef(apply: xml.Elem => Seq[xml.Elem]) extends QueryNode {
    def typeId(): String = "def"
  }
  case class QueryPath(next: () => Seq[Queryable]) extends QueryNode {
    def typeId(): String = "path"
  }

  private class QueryParser extends JavaTokenParsers {
    def queryPath: Parser[(Seq[Prop],Option[xml.Elem])] =
      repsep(query, ".") ~ opt(white ~ "=" ~> white ~> queryValue) ^^ {
        case path ~ value => (path,value)
      }

    def query: Parser[Prop] =
      queryName ~ opt(queryFilters) ^^ {
        case name ~ Some(filters) => (name,filters)
        case name ~ None          => (name,Map())
      }

    def queryName: Parser[String] = """([a-zA-Z][-a-zA-Z]*)|[\?]""".r

    def queryFilters: Parser[Map[String,String]] =
      "[" ~ white ~> repsep(queryFilter, white ~> "," <~ white) <~ "]" ^^ {
        (Map() ++ _)
      }

    def queryFilter: Parser[(String,String)] =
      filterKey ~ "=" ~ filterValue ^^ {
        case key ~ "=" ~ value => (key,value)
      }

    def queryValue(): Parser[xml.Elem] =
      new Parser[xml.Elem] {
        def apply(in: Input): ParseResult[xml.Elem] = {
          // manually parse xml element from text
          val (begin,end) = (in.offset,in.source.length)
          if (begin >= end) {
            Error("Empty value", in)
          }
          else {
            val elem = toXml(in.source.subSequence(begin,end).toString)
            Success(elem, in.drop(end - begin))
          }
        }
      }

    def filterKey: Parser[String] = """[a-zA-Z][-a-zA-Z]*""".r

    def filterValue: Parser[String] =
      "'" ~> """[^']+""".r <~ "'" ^^ {
        (value: String) => value
      }

    def white: Parser[Option[String]] = opt("""[ \t]+""".r)
  }
}

An example usage

For the demonstration, lets define a few classes that implements the Queryable trait.

case class Song(title: String, composer: String, duration: String)
  extends Queryable {
  import Queryable._

  queryMap += (
    "title" -> ("The song's title." -> QueryVal(
      read = () => {{title} :: Nil}
    )),
    "composer" -> ("The song's composer." -> QueryVal(
      read = () => {{composer} :: Nil}
    )),
    "xml" -> ("Transform into xml." -> QueryVal(
      read = () => { :: Nil}
    ))
  )
}

class Jukebox extends Queryable {
  import Queryable._

  private var status = "idle"
  private var songs  = List.empty[Song]

  def queue(song: Song) { songs = song :: songs }

  queryMap += (
    "xml" -> ("Transform into xml." -> QueryVal(
      read = () => { :: Nil}
    )),
    "status" -> ("Status of jukebox." -> QueryVal(
      read = () => {{status} :: Nil}
    )),
    "songs" -> ("Traverse to songs." -> QueryPath(
      next = () => {songs}
    )),
    "queue" -> ("Queues a song from xml." -> QueryDef(
       apply = (elem: xml.Elem) => {
         val song = Song(
           title    = (elem \ "@title").text,
           composer = (elem \ "@composer").text,
           duration = (elem \ "@duration").text
         )
         songs = song :: songs
         done :: Nil
       }
    ))
  )
}

class App extends Queryable {
  import Queryable._

  var status = "ready"
  val jukebox = new Jukebox

  queryMap += (
    "xml" -> ("Transform into xml." -> QueryVal(
      read = () => { :: Nil}
    )),
    "jukebox" -> ("Traverse to jukebox." -> QueryPath(
      next = () => {jukebox :: Nil}
    ))
  )
}

Before demonstrating the Queryable trait using the App, Jukebox, and Song classes, lets also create an utility function for printing out the Either values that Queryable.query() returns.

def print(value: Either[String,xml.Elem]) {
  val printer = new xml.PrettyPrinter(width=80,step=2)
  value match {
    case Right(result) => println(printer format result)
    case Left(error)   => println(error)
  }
}

Lets start by creating the app and query which properties it and the jukebox has.

val app = new App
print(app query "?")
print(app query "jukebox.?")

  
  
  


  
  
  
  
  

Now, lets add some songs programatically and query the jukebox for status and songs.

app.jukebox.queue(Song(title="Mars", composer="Gustav Holst", duration="7m05s"))
app.jukebox.queue(Song(title="Jupiter", composer="Gustav Holst", duration="7m51s"))
print(app query "jukebox.status")
print(app query "jukebox.songs.xml")

  idle


  
  

And a narrow search against specific songs using filtering (note that filter values are treated as regexes).

print(app query "jukebox.songs[title='Ma',composer='Gustav'].xml")

  

Lets queue a new song on the jukebox by specifying its data in xml.

print(app query ("jukebox.queue = " + ))

  done

And finally, searching for the included song.

print(app query "jukebox.songs[composer='Claude'].xml")

  

In summary

Fairly little code was necessary to integrate a querying mechanism into objects. Both the parser combinators and the collection library were of great help. Now, the parser combinators aren’t know to be high performance but that is hardly a problem with the small chunks of text being parsed. There are still some places where this solution is lacking though, like filtering being restricted to single-valued properties, lack of validation, and the dependence on a specific data-format like xml. Maybe the trait should be more asynchronous? This is still work in progress and might receive some updates later on.

If you have any suggestions please drop a comment!

no responses for Making objects queryable in Scala

    Leave a Reply

    Your email address will not be published. Required fields are marked *