Elasticsearch language plugin for Brainfuck scripting

We as a programmers like some programming languages and some of them… we are not interested in at all. It hurts so badly when you don’t have an option to use your lovely language when you clearly see how beautiful solution written in this language will be. Sometimes you have to resign to your fate. But not this time!

Let’s say you work with elasticsearch and you have to write some script. You take a look at the list of supported languages out of the box, than at list of plugins (same link) which add other languages. But no, groovy, javascript, python or closure are in your blacklist for some reasons. There is an option for you! Write your own plugin to extend elasticsearch functionality and you will be able to write scripts in preferred language. Brainfuck for instance!

This post is the continuation of the previous Elasticsearch, Scala, Gradle. Writing plugin step-by-step post. We are going to add the support of another scripting language to elasticsearch. It is going to be simples and minimal working solution to evaluate brainfuck scripts. Don’t you think to use it in production?!

Brainfuck is “esoteric” language from 90s and its interpreter looks quite simple in Scala, just 40 lines (thanks to Peter Braun). I’m going to use slightly changed version of his implementation.

Create plugin class as described in the previous post, replace RestModule by ScriptModule, change name() and description() to correspond the plugin purpose and implement onModule method body registering addScriptEngine. Final class will look like the following (pretty concise in scala):

class BrainfuckPlugin extends AbstractPlugin {
  override def name(): String = "lang-brainfuck"
  override def description(): String = "Adds support for writing scripts in Brainfuck"
  def onModule(module: ScriptModule): Unit = module.addScriptEngine(classOf[BrainfuckScriptEngineService])
}

Create class BrainfuckScriptEngineService that extends AbstractComponent and implements ScriptEngineService interface. Implementation of ScriptEngineService is relatively straightforward: override types and extensions methods to return arrays of appropriate values to define supported scripting language. Final listing of the class without imports:

class BrainfuckScriptEngineService @Inject() (settings: Settings) extends AbstractComponent(settings) with ScriptEngineService {
  override def types(): Array[String] = Array("bf", "brainfuck")
  override def extensions(): Array[String] = Array("brainfuck")

  override def compile(script: String): AnyRef = script
  override def execute(compiledScript: scala.Any, vars: util.Map[String, AnyRef]): AnyRef = {
    BrainfuckEval.eval(compiledScript.asInstanceOf[String].toCharArray)
  }

  override def unwrap(value: AnyRef): AnyRef = value
  override def sandboxed(): Boolean = false

  override def executable(compiledScript: scala.Any, vars: util.Map[String, AnyRef]): ExecutableScript = {
    new BrainfuckExecutableSearchScript(compiledScript.asInstanceOf[String])
  }
  override def search(compiledScript: scala.Any, lookup: SearchLookup, vars: util.Map[String, AnyRef]): SearchScript = {
    new BrainfuckExecutableSearchScript(compiledScript.asInstanceOf[String])
  }
  /*...*/
}

compile(script:String) method should return compiled version of the script but in our case we will return script itself since we implement support of brainfuck just using evaluator. execute(compiledScript, vars) has actual call to evaluate compiledScript but we will ignore vars. unwrap(value:AnyRef) can return value as is, sandboxed() returns false, the rest of the method can be empty except two most interesting methods executable and search which have the same signature as execute method.

They have to return instances of ExecutableScript and SearchScript respectively. For simplicity I will implement those interfaces in one class. For our minimal working solution we have to implement run method which will evaluate script we passed to contractor and methods run{Something} just do simple type cast of the run results to proper type.

class BrainfuckExecutableSearchScript(script: String) extends ExecutableScript with SearchScript {
  override def unwrap(value: scala.Any): AnyRef = value

  override def run(): AnyRef = BrainfuckEval.eval(script.toCharArray)
  override def runAsFloat(): Float = run().asInstanceOf[Float]
  override def runAsLong(): Long = run().asInstanceOf[Long]
  override def runAsDouble(): Double = run().asInstanceOf[Double]

  /*...*/
}

Implementation of brainfuck evaluator as I mentioned already I took from here with one small change instead of printing value I return it.

Let’s build and deploy our plugin to instance of elasticsearch as described in last section of previous post.

You have to enable dynamic scripting: script.inline: on for elasticsearch 1.6+ script.disable_dynamic: false for elasticsearch below 1.6 in config file. For more details see Enable Dynamic Scripting section in official documentation.

And try to use it!

curl -XPOST http://localhost:9200/_search -d '
{
  "aggs": {
    "script": {
      "terms": {
        "lang": "brainfuck",
        "script": "++++++++++[>+++++++>++++++++++>+++<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+."}}}}'

Output of the Http request will be simmilar to:

{
  "aggregations": {
    "script": {
      "doc_count_error_upper_bound": 0,
      "sum_other_doc_count": 0,
      "buckets": [{
        "key": "Hello World!", // <--- result of script evaluation
        "doc_count": 1}]}}}

As you can see the key of the bucket is the result of our brainfuck hello world script evaluation!

References

1) Elasticsearch Scripting 2) Extending the Scripts Module 3) Source code of the plugin 4) Plugin itself Link can be used to install plugin by running command in elasticsearh root directory

bin\plugin --install lang-brainfuck --url https://github.com/mylifeecho/elasticsearch-lang-brainfuck/releases/download/0.0.1/elasticsearch-lang-brainfuck-0.0.1-plugin.zip
blog comments powered by Disqus