Monday, 24 June 2013

Adventures with Scala Macros - Part 4

The Adventures with Scala Macros series was published on the ScottLogic company blog. It is reproduced here.

In the previous articles I built up a macro that generates a RESTful API from a case class model in a Play application. We now want to add a database to this API - in part 3 we used a simple map-based in memory data store, which wouldn’t really be that useful in the real world.

Step up MongoDB, a Scala library for interacting with it, Salat (which uses Casbah under the hood), and a Play plugin for making the integration with our Play application easier, Play-Salat. After following the install instructions, I’ve configured the play application to connect to Mongo:

We’ll use Play-Salat’s example Salat Context, and then we’ll create a set of traits that can be applied to the companion objects for our case classes. However, before we do that we need to change the case classes to use the Casbah ObjectId for their identifiers, so Author becomes:

However, when you try and compile this, Play now gets upset because it doesn’t know how to turn an ObjectId into JSON. A quick read of the documentation and we see that we need to include an implicit Format[ObjectId] to do the conversion to/from JSON. We can implement this as follows:

However, we want this value to be available in the implicit scope in the middle of our macro that creates the formatter using the Play JSON macro, so how can we do that? We’ll look at a way using the macro itself. The way you might do this if you weren’t writing a macro is put the implicit value in an object, and then import myObject._ to get the value into scope wherever we use it. Well, we can easily define a class that can be instantiated to form such an object:

Now we need to tell the macro what class it needs to use to get the implicit values - which we can use a type parameter for:

Which would be declared on the the macro:

By declaring the T on the implementation of the macro as c.WeakTypeTag, we can use it within the macro to get at the actual class that is being used as a type parameter using the implicitly function:

This gives us the ClassSymbol for the type parameter, which we can then use in a Ident to get at the class’s constructor, define a value for it, and then import all its values:

This is basically defining a value as the result of calling the no-argument constructor on the class from the type parameter. The result looks like this:

So now our Play application compiles again. Let’s now move on to creating the Mongo DAOs. First off, we need an abstract class for our companion objects to extend. From the Salat docs, we can see that the class needs to extend ModelCompanion for type parameter T, and needs an implicit Manifest[T]. The SalatDAO requires that T extends AnyRef, so we’ll add a bound for the type parameter. We can then create a dao value that is created using a mongoCollection. All we need then is an abstract collectionName to use for each class’s collection:

Now we can create Mongo traits for the CRUD operations. Let’s start with Read:

This is pretty simple - a template value is defined for the dao (that will be supplied by the MongoDb abstract class), and an Option for the object is found using the id. Let’s do another one - Delete is slightly more complicated, as we want to only delete if a record is found - a simple match expression should do the trick:

Now let’s look at Write. We’re need to take an object of type T, find the corresponding record, and update its state to that provided. To find the record we need the ID, but the object we have is of type T <: AnyRef, so we don’t have access to its ID. Simple enough - we introduced the WithId abstract class in the last article - we can update it to use ObjectId, and make sure the type parameter is bound to extend AnyRef:

Now we can use this in the MongoWrite trait to get the id:

I’m sure you’ve got the hang of these traits now - they’re pretty simple. Here’s the rest of those that we need:

Finally, we add the new traits to the companion objects, adding just the ones we want for each case class:

So that’s it - a RESTful API backed by MongoDB with the minimum of code using Scala def macros. I hope you’ve found this introduction interesting, and if you would like to browse the source code, please head over to Github.

No comments:

Post a comment