Kotlin flexibility makes it ideal for the creation of Domain Specific Languages, or DSLs. In this tutorial, we examine the establishment of a DSL in Kotlin.
- [Instructor] A domain-specific language or DSL is a computer language which is focused on a very narrow problem set. This is unlike general purposes languages like Kotlin, which can solve a wide variety of problems. We are going to create a very small DSL to help us unit test Kotlin code. We will only build a small piece of a bigger solution. Let's create a new file. Right-click on comm.tekadept.demo in the project window. Choose New > Kotlin File/Class.
Name the file DSL, and add a package to the top. While traditional unit test frameworks use assert, many modern ones strive to make the test readable. So instead of having assert or assertTrue, Kotlin has many features which can help us to achieve a more English-like sentence structure. We begin by defining an interface named matcher.
So we say interface Matcher, and this is going to be a generic type, so we'll give it a T. Inside of here we're going to have a function called test, and we're going to say lhs for left-hand side, and this is also going to be of type T. And it's going to return a type Unit, and Unit, once again, is Kotlin's version of a Java void.
And then we're going to have an infix function or. Or is the name of the function. And then we're going to says other: Matcher, and this, again, is a generic, so it's going to be of type T. And it will return a Matcher of type T. And we'll have equals object, and another Matcher of type T.
Now, all of this is going to be overrideable. So we say override fun test(lhs), type T, and then, because we're creating a unit test, exceptions can occur while the test is running. So we want to be able to trap those, and we want to trap those in an exception. So we're going to say try this@Matcher.test, and the parameter we're going to give test is lhs.
If something goes wrong, we're going to catch it, and we're going to say e:, and this would be a RuntimeException, and we'll say other.Test(lhs). So this is our interface for our unit test framework. Next, we define two versions of a should method. Again, what we're trying to do is make things read like English.
One works with single types, and the other works on collections. Both are going to use the infix keyword. It allows us to move the function in between its parameters. This is similar to the what the plus operator sits between its two operands. So again, we say infix function. It's going to be of type T, and we're going to say should, and again, our parameter here is going to be called matcher, and it's going to be of type Matcher of T.
Open curly brace, matcher.test, and what matcher's going to test is the current object. So we're going to pass this. And that right there is the should for a single object. Now, we'll do the one for our collection. Infix function of T. Now we're going to have a Collection of T, dot, should and the parameter is going to be fn for function, CollectionMatchers, and that's Matchers with an S.
It's going to be red right now because we haven't actually created that class yet, of T, and then we're going to say dot, open, close, paren, and any time you see the open, close paren and an arrow, that's indicating that we're going to pass it a function. And we're going to say val matchers equals CollectionMatchers, and again, we're going to pass it this.
And the final part for this function is matchers.fn, and we'll call the function that got passed into this. And then we create a CollectionMatchers class. This class holds the clause, which comes after the word should. So it'll have something like should contain, should not contain, should have a size less than this. We'll say class CollectionMatchers<T> (val collection: Collection<T>).
I have a misspelling there. Let's fix that. Now, this class is going to contain all our different matchers. So the first one's going to be fun contains, and we're going to call this rhs for right-hand side of T, and it's going to return a type of Unit. and we're going to say if not, check the collection to see if it passes the test or not, contains(rhs).
If it doesn't contain the item we're looking for, we're going to throw a Runtime exception. And give the user a useful message. Let's do one more. Let's do the opposite one. Fun notContain, and again, right-hand side. The type is T.
Returns a Unit. Open curly brace, close curly brace, and this one looks very similar to the one that we just did, except it's going to be if the collection contains right-hand side, throw a Runtime exception. Collection should not contain.
And our final thing, we'll do one last matcher, and this is going to be just a slightly different kind of matcher. And we're going to say, haveSizeLessThan, and this one's going to be an integer. It's going to return a type of Unit, and if(collection.size >= size), then we're going to throw a Runtime exception.
And give the user a message, Collection should have size less than $size. Okay, so that pretty much describes our class. Okay, looks like we have a little error. We're missing an R right here. And so we go ahead and add that R to matchers. And now, all we want to do, let's create a function to hold our unitTest.
And we're going to say val listOfNames = listOf, and we'll come up with a couple of names. April, May, and June. And then we'll actually write our unit test in our DSL. So we'll say listOfNames should.
This time, we have to put a function literal, and we're going to say notContain, and for whatever reason, it shouldn't contain Portia. If the list of names contain Portia, a Runtime exception will be thrown. Take note of the way that the code reads, almost like an English sentence. With a bit more hard work, we can improve its grammar even more.
- Kotlin as a better Java
- Setting up a Kotlin programming environment
- Val vs. var
- Understanding basic Kotlin programming concepts
- Object-oriented programming
- Using Java from Kotlin
- Using Kotlin from Java
- Annotations, reflection, and DSL construction
- Functional programming in Kotlin