Devise Ways to Run DSLs from External Sources

A DSL may be embedded within a Kotlin .kt file, a Kotlin script .kts file, or may be provided in a standalone file. Typically a DSL intended for use by Kotlin programmers may make use of the first two options—they can naturally place the code along with other Kotlin code in a .kt or .kts file. However, a DSL intended to be used by nonprogrammers or to be used in isolation may be placed in a separate text file, or the content may even arrive by some other means.

In the examples so far, we compiled the supporting code needed to process DSL code into class files or into JAR files. Then we exercised the DSL snippets by running kotlinc-jvm with the -script option. That’s a reasonable approach if we merely want to run the DSL code snippets. In general, however, DSLs are often used in the context of larger enterprise applications. Thankfully, exercising DSL code snippets from within Kotlin or Java applications is relatively easy, as we’ll see here.

A user may provide us DSL code in one of several ways, depending on the use cases supported by applications. The DSL code may be:

  • Created as a text file
  • Input directly into the application via a dialog or console
  • Received securely from a remote service via an HTTP request
  • Read from a database
  • Generated by a third-party application

Irrespective of the source, we should be able to load the code snippet into a String. To be able to process a DSL code snippet in an application, we simply need to figure out a way to evaluate or process code stored in a String and be able to get a response from that processing into our application.

The JVM already has a way to process scripts written in different languages—for example, JavaScript or Groovy—from within Java or JVM applications written using other JVM languages. This capability comes through a scripting engine library that’s part of JSR-223.[4] Since Kotlin runs on the JVM, we can use the same mechanism to execute scripts written in other languages from within a Kotlin application.

To dynamically process Kotlin code through the scripting engine library, we need an implementation of a ScriptEngineManager for Kotlin scripts. That’s available in a class, org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory, implemented by JetBrains, the company behind the Kotlin language.

Since we need a few different libraries to get this working, it’s easier to set up a project using either Gradle or Maven. We’ll use Gradle here. You can download the code for this example from the website[5] for this book. Once you download and unzip the file, look in the code/embed directory.

First, let’s take a look at the Gradle build.gradle.kts file which, in itself, happens to be a DSL written using Kotlin.

 plugins {
  kotlin(​"jvm"​) version ​"1.3.72"
  application
 }
 
 application {
  mainClassName = ​"com.agiledeveloper.dsl.RunDSLKt"
 }
 
 repositories {
  mavenCentral()
 }
 
 dependencies {
  implementation(kotlin(​"stdlib"​))
  implementation(kotlin(​"reflect"​))
  implementation(kotlin(​"script-runtime"​))
  implementation(kotlin(​"compiler-embeddable"​))
  implementation(kotlin(​"script-util"​))
  runtime(kotlin(​"scripting-compiler-embeddable"​))
  runtime(​"net.java.dev.jna:jna:5.5.0"​)
 }
 
 val​ run: JavaExec ​by​ tasks
 run.standardInput = System.`in`
 
 defaultTasks(​"clean"​, ​"run"​)

The build file specifies the Kotlin plugin and the application plugin. It refers to a class, RunDSLKt, which will contain the main() function. The dependencies section specifies the different libraries we need to get this code working. Mainly, we’re referring to the Kotlin scripting, runtime, and compiler libraries. Finally, the default task executes the clean task and the run task, which will run the main() function from the specified main class.

We’ll read in DSL snippets that specify meeting schedules and run them from within a Kotlin application. We need the schedule singleton, the meeting() function, and the Meeting class we wrote before. We’ll take the version of the meeting.kt file from Proactively Handle Errors, and modify it slightly as shown here:

 package​ ​com.agiledeveloper.dsl
 
 object​ schedule {
 infix​ ​fun​ ​meeting​(block: Meeting.() -> Unit) =
  Meeting().apply(block).validate()
 }
 
 class​ Meeting {
 //rest of the code same as before...

We implemented three changes in this version of the meeting.kt file:

  1. We moved the file into the directory src/main/kotlin/com/agiledeveloper/dsl, a directory structure that both Gradle and Maven expect by default.

  2. We added a package declaration at the top of the file.

  3. Finally, in the meeting() function, instead of printing the details in the Meeting instance, we return the instance to the caller.

The rest of the code in meeting.kt is the same as in the previous version from Proactively Handle Errors.

We have the code to process the DSL in place, but we need a piece of code to emulate a real-world application. That glue code will read in the DSL code snippet, execute it within the safe haven of exception handling, and, finally, report the response or any errors gracefully. We’ll create the necessary code for that in a file named RunDSL.kt:

 package​ ​com.agiledeveloper.dsl
 
 fun​ ​main​() {
  print(​"Please enter the filename for schedule:"​)
 
 val​ fileName = readLine()
 
 val​ dsl = java.io.File(fileName).readText()
 
 val​ scriptEngine =
  javax.script.ScriptEngineManager().getEngineByExtension(​"kts"​)
 
 try​ {
 val​ result = scriptEngine.eval(​"""
  import com.agiledeveloper.dsl.schedule
 
  $dsl
  """​)
 
  println(​"Result of evaluation of the DSL:"​)
  println(result)
  } ​catch​(ex: Exception) {
  println(​"""OOPS, we ran into an error.
  ${ex.message}"""​)
  }
 }

The code in this file also belongs to the same package as the schedule singleton and the Meeting class. In the main() function, we ask the user to provide the name of a file containing the DSL code snippet. Instead of reading from a file, we may ask the user to key in the DSL, fetch the DSL from a database, get it from a remote service, and so on.

We read the content of the DSL file, using the readText() extension function added by the Kotlin standard library to the java.io.File class, and store the code snippet into a String variable named dsl. Then we get an implementation of the ScriptEngine for Kotlin script, kts, by calling the getEngineByExtension() function on the ScriptEngineManager.

As a final step, we prefix an import line to the code snippet we read from the file and ask the instance of the ScriptEngine to execute it by calling the eval() function. If all goes well, the result of the call, a Meeting object, will be stored in the result variable. If something were to go wrong, the details of the exception are available via the variable ex.

The approach you use to integrate DSL processing into your own enterprise applications can be similar. Instead of a main() function, you can write the code to invoke the ScriptEngine from anywhere in your application, as appropriate.

We need one more file to make all this work well together. We need to specify, in a manifest file, the name of the class implementing the ScriptEngine interface for Kotlin. Create a file, src/main/resources/META-INF/services/javax.script.ScriptEngineFactory, and key the following details in it:

 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory

We’re all set to take this application to process DSL code snippets for a ride. Let’s try it out with a few different code snippets.

Let’s start with a good file, with no errors in it.

 schedule meeting {
  assign name ​"Meeting to discuss why meetings aren't effective"
  starts at 14..30
  ends at 15..30
  on date 15 March 2020
  participants include ​"Sara"​ and ​"Jake"​ and ​"Mani"
 }

Unlike the previous examples where we placed the DSL code into .kts files, in this example we placed the DSL code into a file named schedulegood.dsl. We used a .dsl extension, but you may use whatever extension you like for this file, or no extension at all.

To run the application, you can use the Gradle wrapper to quickly install Gradle as part of compiling and executing the application. The Gradle wrapper is part of the source code that you can download from the website of the book.

To execute the application, at the command prompt, run this command from the code/embed directory:

./gradlew

If you’re on Windows, run this command instead:

gradlew.bat

When asked, enter the name of the file as schedulegood.dsl. The interactive session and the output is shown here:

 Please enter the filename for schedule:schedulegood.dsl
 
 Result of evaluation of the DSL:
 Meeting: Meeting to discuss why meetings aren't effective
 Starts at 14:30
 Ends at 15:30
 On 2020-03-15
 Participants: Sara, Jake, Mani

That worked well. We can see the output from our application with the details gathered from the instance of Meeting returned by the schedule’s meeting() function.

Let’s now try a DSL snippet with a syntax error in it:

 schedule meeting {
  assign name ​"Meeting to discuss why meetings aren't effective"
  starts at 14..30
  end at 15..30
  on date 15 March 2020
  participants include ​"Sara"​ and ​"Jake"​ and ​"Mani"
 }

Run ./gradlew again, but this time enter schedulesyntaxerr.dsl as the name of the file. The output is shown here:

 Please enter the filename for schedule:schedulesyntaxerr.dsl
 
 OOPS, we ran into an error.
  error: unresolved reference: end
  end at 15..30
  ^

The syntax error in the code snippet was gracefully handled by our application and we can alert the user about the error and guide them to fix the issues.

We can similarly deal with logical errors. Let’s use the following DSL code snippet:

 schedule meeting {
  assign name ​"Meeting to discuss why meetings aren't effective"
  starts at 14..30
  starts at 14..30
  on date 15 March 2020
  participants include ​"Sara"​ and ​"Jake"​ and ​"Mani"
 }

Run ./gradlew again and key in the file name as schedulelogicalerr.dsl to see this output:

 Please enter the filename for schedule:schedulelogicalerr.dsl
 
 OOPS, we ran into an error.
  java.lang.RuntimeException: Meeting not set up properly:
 duplicate start time
 end time not set

In this example, we saw how to bring DSL code snippets into an application for processing. Even though the DSL code came from a standalone file, we were able to run the snippets in the context of classes and functions that know how to properly process it. In addition to being able to run the code within an application, we also handled the failures gracefully.

That brings us to the end of this journey of learning to create DSLs in Kotlin. Every end is a new beginning, and I hope this book gave you a good boost on your voyage of creating your own DSLs.

Let me end by saying have fun designingYourOwnDSLs();

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.81.94