Loading...

December 17, 2015

Ratpacked: Special Routing Of Promise Values Using Predicates

One of the strengths of Ratpack is the asynchronous execution model. An important class is the Promise class. An instance of the class will represent a value that is available later. We can invoke several operations that need be applied to a value when a Promise is activated. Usually the activation happens when we subscribe to a Promise using the then method. We can use the route method for a Promise to have a different action when a certain predicate is true. The action will stop the flow of operations, so methods that are executed after the route method are not executed anymore if the predicate is true. If the predicate is false then those methods are invoked.

The Promise class has a method onNull as a shorthand for the route method where the predicate checks if the value is null. For example we could have a service in our application that returns a Promise<User>. If the value is null we want some special behaviour like sending a 404 status code to the client. With the following code we could achieve this:

import ratpack.jackson.Jackson

import static ratpack.groovy.Groovy.ratpack

ratpack {
    bindings {
        bind(UserService, DefaultUserService)
    }
    handlers { UserService userService ->
        get('user/:username') { 
            final String username = pathTokens.username
            userService
                    .findUser(username)
                    // If user is not found, then value
                    // of Promise object is null. 
                    .onNull { clientError(404) }
                    .map(Jackson.&json)
                    .then { JsonRender userAsJson ->
                        render(userAsJson)
                    }
        }   
    }
}

In the following Spock specification we can see both the onNull and route methods and how they work:

package com.mrhaki.ratpack.promise

import ratpack.exec.Promise
import ratpack.func.Predicate
import ratpack.test.exec.ExecHarness
import spock.lang.AutoCleanup
import spock.lang.Specification

class PromiseValueRoutingSpec extends Specification {

    @AutoCleanup
    ExecHarness execHarness = ExecHarness.harness()
    
    def "when Promise value is null then special action is used"() {
        given:
        String execResult

        when:
        execHarness.run({
            Promise.value(promiseValue)
                // If Promise has null value then 
                // execute the action, otherwise
                // proceed.
                .onNull {
                    execResult = 'null value' 
                }
                // Promise value was not null, so
                // we get to this action block
                .then { String value ->
                    execResult = value 
                }
        })

        then:
        execResult == result

        where:
        promiseValue    || result
        'Ratpack rules' || 'Ratpack rules'
        ''              || ''
        null            || 'null value'
    }

    def "when a Promise value meets a predicate a custom action is used"() {
        given:
        String execResult
        
        and:
        // Create Ratpack predicate for checking if Optional object
        // is empty.
        final Predicate<Optional<String>> emptyOptional = { Optional<String> optional ->
            // Check if value is present
            !optional.present
        } 

        when:
        execHarness.run({
            Promise.value(promiseValue)
                // If Promise has null value then 
                // execute the action, otherwise
                // proceed.
                .onNull {
                    execResult = 'null value' 
                }
                // Custom check for the Promise value
                // which executes the given action when
                // the predicate is true.
                .route(emptyOptional) { 
                    execResult = 'empty optional value' 
                }
                // Promise value was not null and
                // optional value was present, so
                // we get to this action block
                .then { Optional<String> optional ->
                    execResult = optional.get() 
                }
        })

        then:
        execResult == result

        where:
        promiseValue                 || result
        Optional.empty()             || 'empty optional value'
        Optional.of('Ratpack rules') || 'Ratpack rules'
        null                         || 'null value'
    }
}

Written with Ratpack 1.1.1