A scala library that enable the creation of contextual DSLs, inspired by the routing module of Spray.
It uses part of the internal code of spray-routing and its directives, but enables to use custom contexts and create a context-specific DSL.
Scalext is available on Maven Central
(since version 0.4.0
), and it is cross compiled and published for Scala 2.12, 2.11 and 2.10.
Older artifacts versions are not available anymore due to the shutdown of my self-hosted Nexus Repository in favour of Bintray
Using SBT, add the following dependency to your build file:
libraryDependencies ++= Seq(
"io.bfil" %% "scalext" % "0.4.0"
)
If you have issues resolving the dependency, you can add the following resolver:
resolvers += Resolver.bintrayRepo("bfil", "maven")
To build your own DSL all you need to do is:
- define your custom context object
- define the DSL actions
Let's use a simple calculator DSL as an example.
The calculator DSL context will be a simple class that stores a promise object (which will resolve into a future result at some point) and the current calculation value.
The ArihmeticContext
object can be a simple case class:
case class ArithmeticContext(resultPromise: Promise[Double], value: Double)
The DSL actions will manipulate this context, we can put the actions in a CalculatorDsl
trait:
trait CalculatorDsl extends ContextualDsl[ArithmeticContext] {
// actions in here..
}
The first action triggering the DSL logic needs to accept an Action
as a parameter.
In this example we create an initial action startWith
that accepts an initial value, it creates the ArithmeticContext
storing the initial value and a promise, and it passes the newly created context into the action specified.
def startWith(initialValue: Long)(action: Action) = {
val p = Promise[Double]
action(ArithmeticContext(p, initialValue))
p.future
}
The above method returns the future associated with the promise, so that we can resolve the promise at a later point when we want to return a result.
Let's define some basic actions for the DSL (add
, subtract
, multiplyBy
, and divideBy
), the actions use the mapContext
helper method of the ContextualDsl
trait, which modifies the immutable context and passes it through to the inner action.
def add(value: Double) = mapContext(ctx => ctx.copy(value = ctx.value + value))
def subtract(value: Double) = mapContext(ctx => ctx.copy(value = ctx.value - value))
def multiplyBy(value: Double) = mapContext(ctx => ctx.copy(value = ctx.value * value))
def divideBy(value: Double) = mapContext(ctx => ctx.copy(value = ctx.value / value))
The inner most method of the DSL usually must be a simple action that takes the context object and returns Unit, what we want to do is completing the promise by passing in it the result of our calculation.
def returnResult = ActionResult { ctx => ctx.resultPromise.completeWith(Future { ctx.value }) }
We called our final action returnResult
, and we defined it using the apply method of the ActionResult
object, which makes the code more readable, but it could just be a simple method with the signature Context => Unit
.
Finally using our CalculatorDsl
in our code will look like this:
class Calculator extends CalculatorDsl {
def performCalculation: Future[Double] =
startWith(2) {
add(3) {
multiplyBy(3) {
subtract(5) {
divideBy(2) {
returnResult
}
}
}
}
}
}
This is just a simple example of how the library can be used to create DSLs that manipulate a context.
The main components of Scalext are the ContextualDsl
trait and the DSL actions.
The ContextualDsl
trait accepts a context type parameter
trait ContextualDsl[T]
Extending the ContextualDsl
trait enables the creation of DSL actions
case class ExampleContext()
trait MyDsl extends ContextualDsl[ExampleContext] {
// actions in here..
}
DSL Actions are methods that accepts a context and return Unit
type Action = Context => Unit
The following DSL actions can be used as basic helpers to create other DSL actions
mapContext
mapContext(f: Context => Context)
It maps the context to a new context and passes it into the inner action:
mapContext ( ctx => updateContext(ctx) ) {
ctx => Unit
}
provide
provide[T](value: T)
It provides/passes a value into the inner action:
provide ( "hello" ) { str =>
ctx => println(str) // hello
}
extract
extract[T](f: Context => T)
It extracts a value from the context and passes it into the inner action:
extract ( ctx => ctx.total ) { total =>
ctx => println(total)
}
onComplete
onComplete[T](f: => Future[T])(implicit ec: ExecutionContext)
onComplete[T](f: Context => Future[T])(implicit ec: ExecutionContext)
It waits for the completion of a future a passes the future result into the inner action:
onComplete ( Future { "hello" } ) {
case Success(str) => ctx => println(str) // hello
case Failure(ex) => throw ex
}
It can also accept as a parameter a function that returns a future from a context:
onComplete ( ctx => Future { "hello" } ) {
case Success(str) => ctx => println(str) // hello
case Failure(ex) => throw ex
}
onSuccess
onSuccess[T](f: => Future[T])(implicit ec: ExecutionContext)
onSuccess[T](f: Context => Future[T])(implicit ec: ExecutionContext)
It waits for the completion of a future a passes the future result if successful into the inner action:
onSuccess ( Future { "hello" } ) { str =>
ctx => println(str) // hello
}
It can also accept as a parameter a function that returns a future from a context:
onSuccess ( ctx => Future { "hello" } ) { str =>
ctx => println(str) // hello
}
onFailure
onFailure[T](f: => Future[T])(implicit ec: ExecutionContext)
onFailure[T](f: Context => Future[T])(implicit ec: ExecutionContext)
It waits for the completion of a future a passes the future result if successful into the inner action:
onFailure ( Future { throw new Exception("Future failure") } ) { ex =>
ctx => throw ex
}
It can also accept as a parameter a function that returns a future from a context:
onFailure ( ctx => Future { throw new Exception("Future failure") } ) { ex =>
ctx => throw ex
}
after
after[T](delay: FiniteDuration)(implicit ec: ExecutionContext, ac: ActorContext)
It exectues the inner action after a given delay:
after ( 3 seconds ) {
ctx => Unit
}
All the actions above have same result type, which extends the following abstract class:
abstract class ChainableAction[L]
To create a custom action you can use the following 2 helper types:
type ChainableAction0 = ChainableAction[Unit]
type ChainableAction1[T] = ChainableAction[Tuple1[T]]
The first type returns a chainable action, which will call the inner action by passing only the context as an argument, while the second one will pass a custom value of type T that can be used by the inner action.
Let's use mapContext
and provide
as an example that return the 2 chainable action types:
def mapContext(f: Context => Context): ChainableAction0
def provide[T](value: T): ChainableAction1[T]
The ChainableAction0 will then have an apply method with the following signature:
def apply(fn: Action)
Where Action
is a shorthand type for Context => Unit
.
While for ChainableAction1[String]
would be:
def apply(fn: String => Action)
To define a custom chainable action we can use the apply method defined on the ChainableAction
companion object:
object ChainableAction {
def apply[T: Tuple](f: (T => Action) => Action): ChainableAction[T]
}
or we can define the tapply method part of the abstract class ChainableAction
directly:
abstract class ChainableAction[L](implicit val ev: Tuple[L]) {
def tapply(f: L => Action): Action
}
Here's an example of a custom action that returns a ChainableAction0
, which prints "hello" and then calls the inner action:
// define it
def printHello: ChainableAction0 = ChainableAction { inner =>
ctx =>
println("hello")
inner(())(ctx)
}
// then use it
printHello {
ctx => ()
}
Here's an example of a custom action that returns a ChainableAction1[Int]
, which passes a random integer to the inner action:
// define it
def randomize: ChainableAction1[Int] = ChainableAction { inner =>
ctx =>
val randomInt = scala.util.Random.nextInt
inner(Tuple1(randomInt))(ctx)
}
// then use it
randomize { randomInt =>
ctx => println(randomInt)
}
- scalescrape - an actor-based web scraping library
This software is licensed under the Apache 2 license, quoted below.
Copyright © 2014-2017 Bruno Filippone http://bfil.io
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
[http://www.apache.org/licenses/LICENSE-2.0]
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.